package com.lenovo.lcdmoauth.config;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.RequestMatcher;

import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.RSAPrivateKeySpec;
import java.security.spec.RSAPublicKeySpec;
import java.time.Duration;
import java.util.List;
import java.util.Map;
import java.util.UUID;

/**
 * Spring Authorization Server 配置
 *
 * @author lihc14
 */
@Slf4j
@Configuration
@EnableWebSecurity
//@EnableRedisIndexedHttpSession
public class AuthorizationServerConfig {
    private final static int PORT_80 = 80;
    private final static int RSA_KEY_LENGTH = 2048;

    @Autowired
    UserDetailsService userDetailsService;

    @Autowired
    JdbcTemplate jdbcTemplate;

//    @Autowired
//    private RedisUtil redisUtil;

//    @Autowired
//    AuthTokenCache authTokenCache;

    @Value("${server.port}")
    private int port;

    /**
     * 忽略授权的地址列表
     */
    @Value("${plm.security.permit}")
    private String[] permits;

    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http, JdbcTemplate jdbcTemplate) throws Exception {
        OAuth2AuthorizationServerConfigurer authorizationServerConfigurer =
                new OAuth2AuthorizationServerConfigurer();
        RequestMatcher endpointsMatcher = authorizationServerConfigurer
                .getEndpointsMatcher();
        //默认不支持OpenID Connect 1.0协议，需要进行配置
        authorizationServerConfigurer.oidc(oidcConfigurer -> {
            oidcConfigurer.clientRegistrationEndpoint(Customizer.withDefaults());
        });
        http.securityMatcher(endpointsMatcher)
                .authorizeHttpRequests(authorize ->
                        authorize.anyRequest().authenticated()
                ).csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
                .apply(authorizationServerConfigurer);
        http.formLogin(Customizer.withDefaults());

        ItcodeGrantAuthenticationConverter converter = new ItcodeGrantAuthenticationConverter();
        ItcodeGrantAuthenticationProvider provider = new ItcodeGrantAuthenticationProvider();

        CustomRevocationAuthenticationProvider customRevocationProvider = new CustomRevocationAuthenticationProvider();
        CustomRevocationAuthenticationConverter customRevocationAuthenticationConverter = new CustomRevocationAuthenticationConverter();

        PasswordAuthenticationConverter passwordConverter = new PasswordAuthenticationConverter();
        PasswordAuthenticationProvider passwordProvider = new PasswordAuthenticationProvider();

        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
                // 让认证服务器元数据中有自定义的认证方式
                .authorizationServerMetadataEndpoint(metadata -> metadata.authorizationServerMetadataCustomizer(customizer -> {
                    customizer.grantTypes(config -> {
                        config.add(SecurityContants.ITCODE_GRANT_TYPE);
                        config.add(SecurityContants.PASSWORD_GRANT_TYPE);
                    });
                }))
                // 添加自定义grant_type
                .tokenEndpoint(tokenEndpoint -> tokenEndpoint.accessTokenRequestConverters(config -> {
                    config.add(converter);
                    config.add(passwordConverter);
                }).authenticationProviders(config -> {
                    config.add(provider);
                    config.add(passwordProvider);
                }))
                .tokenRevocationEndpoint(oAuth2TokenRevocationEndpointConfigurer -> oAuth2TokenRevocationEndpointConfigurer.revocationRequestConverter(customRevocationAuthenticationConverter).authenticationProvider(customRevocationProvider));

        DefaultSecurityFilterChain build = http.build();
        OAuth2TokenGenerator<?> tokenGenerator = http.getSharedObject(OAuth2TokenGenerator.class);
        AuthenticationManager authenticationManager = http.getSharedObject(AuthenticationManager.class);
        OAuth2AuthorizationService authorizationService = http.getSharedObject(OAuth2AuthorizationService.class);

        customRevocationProvider.setJdbcOperations(jdbcTemplate);

        provider.setTokenGenerator(tokenGenerator);
        provider.setAuthorizationService(authorizationService);
        provider.setAuthenticationManager(authenticationManager);
        provider.setUserDetailsService(userDetailsService);

        passwordProvider.setTokenGenerator(tokenGenerator);
        passwordProvider.setAuthorizationService(authorizationService);
        passwordProvider.setAuthenticationManager(authenticationManager);
        passwordProvider.setUserDetailsService(userDetailsService);
        return build;
    }

    @Bean
    public SecurityFilterChain httpSecurityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                .authorizeHttpRequests(authorizeRequests ->
                        authorizeRequests.requestMatchers(permits).permitAll()
                                .anyRequest().authenticated()
                )
                .formLogin(fromlogin -> fromlogin.loginPage("/login").permitAll())
//                .authenticationManager(customManager())
                .logout(logout -> {
                    logout.logoutSuccessUrl("/logoutSuccess");
                })
                .csrf(AbstractHttpConfigurer::disable)
                .oauth2ResourceServer(oauth2ResourceServer -> {
                    oauth2ResourceServer.accessDeniedHandler(new CustomAccessDeniedHandler());
                    oauth2ResourceServer.authenticationEntryPoint(new CustomAuthenticationEntryPoint());
                    oauth2ResourceServer.jwt(jwt -> {
                    });
                });
        return httpSecurity.build();
    }

    @Bean
    public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate, PasswordEncoder passwordEncoder) {
        RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
                // 客户端id
                .clientId("lcdm-client")
                .clientName("lcdm-client")
                // 客户端秘钥，使用密码解析器加密
                .clientSecret(passwordEncoder.encode("lcdm-client"))
                // 客户端认证方式，基于请求头的认证
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .redirectUri("http://127.0.0.1:8080/login/oauth2")
                // 配置资源服务器使用该客户端获取授权时支持的方式
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                .authorizationGrantType(SecurityContants.ITCODE)
                .authorizationGrantType(SecurityContants.PASSWORD)
                .authorizationGrantType(SecurityContants.REVOKE)
                .authorizationGrantType(SecurityContants.ITCODE)
                .redirectUri("")
                // 该客户端的授权范围，OPENID与PROFILE是IdToken的scope，获取授权时请求OPENID的scope时认证服务会返回IdToken
                .scope(OidcScopes.OPENID).scope(OidcScopes.PROFILE)
                // 自定scope
                .scope("message.read").scope("message.write")
                // 客户端设置，设置用户需要确认授权
                .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
                .tokenSettings(TokenSettings.builder()
                        .accessTokenTimeToLive(Duration.ofSeconds(2 * 60 * 60))
                        .authorizationCodeTimeToLive(Duration.ofSeconds(2 * 60 * 60))
                        .refreshTokenTimeToLive(Duration.ofSeconds(24 * 60 * 60)).build()).build();

        // 基于db存储客户端，还有一个基于内存的实现 InMemoryRegisteredClientRepository
        JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);

        // 初始化客户端
        RegisteredClient repositoryByClientId = registeredClientRepository.findByClientId(registeredClient.getClientId());
        if (repositoryByClientId == null) {
            registeredClientRepository.save(registeredClient);
        }
        return registeredClientRepository;
    }

    @Bean
    public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
    }

    //
    @Bean
    public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
    }

    @Bean
    public AuthorizationServerSettings providerSettings() {
        String issuerUrl = "http://127.0.0.1";
        if (port != PORT_80) {
            issuerUrl = issuerUrl.concat(":").concat(String.valueOf(port));
        }
        return AuthorizationServerSettings.builder().issuer(issuerUrl).build();
    }

    @Bean
    WebSecurityCustomizer webSecurityCustomizer() {
        return web -> {
            web.ignoring().requestMatchers("/userinfo");
        };
    }

    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    KeyPair generateRsaKey() {
        KeyPair keyPair = null;
        try {
            String sql = "select pub_key PUB,pri_key PRI from oauth2_authorization_jwt order by create_time desc limit 1";
            List<Map<String, Object>> mapList = jdbcTemplate.queryForList(sql);
            if (CollUtil.isEmpty(mapList)) {
                KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
                keyPairGenerator.initialize(RSA_KEY_LENGTH);
                keyPair = keyPairGenerator.generateKeyPair();

                RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
                String pubKey = bigIntegerToHexString(privateKey.getModulus(), RSA_KEY_LENGTH / 4);
                String priKey = bigIntegerToHexString(privateKey.getPrivateExponent(), RSA_KEY_LENGTH / 4);
                sql = "INSERT INTO oauth2_authorization_jwt (id, pub_key, pri_key, create_time) VALUES(?,?,?,now())";
                jdbcTemplate.update(sql, UUID.randomUUID().toString(), pubKey, priKey);
            } else {
                Map<String, Object> mapKey = mapList.get(0);
                String pubKey = mapKey.get("PUB").toString();
                String priKey = mapKey.get("PRI").toString();

                RSAPrivateKeySpec priKeySpec = new RSAPrivateKeySpec(new BigInteger(pubKey, 16), new BigInteger(priKey, 16));
                KeyFactory keyFac = KeyFactory.getInstance("RSA");
                RSAPublicKeySpec pubKeySpec = new RSAPublicKeySpec(new BigInteger(pubKey, 16), new BigInteger("65537"));
                RSAPrivateKey privateKey = (RSAPrivateKey) keyFac.generatePrivate(priKeySpec);
                RSAPublicKey publicKey = (RSAPublicKey) keyFac.generatePublic(pubKeySpec);
                keyPair = new KeyPair(publicKey, privateKey);
            }
        } catch (Exception ex) {
            log.error("init rsa key error.");
            throw new IllegalStateException(ex);
        }
        return keyPair;
    }

    @Bean
    public JWKSource<SecurityContext> jwkSource(KeyPair keyPair) {
        RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
        RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
        RSAKey rsaKey = new RSAKey.Builder(publicKey)
                .privateKey(privateKey).build();
        JWKSet jwkSet = new JWKSet(rsaKey);
        return new ImmutableJWKSet<>(jwkSet);
    }

    @Bean
    public JwtDecoder jwtDecoder(KeyPair keyPair) {
        return NimbusJwtDecoder.withPublicKey((RSAPublicKey) keyPair.getPublic()).build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    private static String bigIntegerToHexString(BigInteger modulus, int length) {
        String hexString = modulus.toString(16).toUpperCase();
        return StrUtil.padPre(hexString, length, "0");
    }
}
